import os
import unittest

import numpy as np
from numpy.testing import assert_array_almost_equal

import skrf as rf


class CircuitTestConstructor(unittest.TestCase):
    """
    Various tests on the Circuit constructor.
    """
    def setUp(self):
        # Importing network examples
        self.test_dir = os.path.dirname(os.path.abspath(__file__))+'/'
        self.ntwk1 = rf.Network(os.path.join(self.test_dir, 'ntwk1.s2p'))
        self.ntwk2 = rf.Network(os.path.join(self.test_dir, 'ntwk2.s2p'))
        self.freq = self.ntwk1.frequency
        # circuit external ports
        self.port1 = rf.Circuit.Port(self.freq, name='Port1')
        self.port2 = rf.Circuit.Port(self.freq, name='Port2')
        # circuit connections
        self.connections = [[(self.port1, 0), (self.ntwk1, 0)],
                       [(self.ntwk1, 1), (self.ntwk2, 0)],
                       [(self.ntwk2, 1), (self.port2, 0)]]
        self.circuit = rf.Circuit(self.connections)

    def test_all_networks_have_name(self):
        """
        Check that a Network without name raises an exception
        """
        _ntwk1 = self.ntwk1.copy()
        connections = [[(self.port1, 0), (_ntwk1, 0)],
                       [(_ntwk1, 1), (self.ntwk2, 0)],
                       [(self.ntwk2, 1), (self.port2, 0)]]

        _ntwk1.name = []
        self.assertRaises(AttributeError, rf.Circuit, connections)

        _ntwk1.name = ''
        self.assertRaises(AttributeError, rf.Circuit, connections)

    def test_all_networks_have_same_frequency(self):
        """
        Check that a Network with a different frequency than the other
        raises an exception
        """
        _ntwk1 = self.ntwk1.copy()
        connections = [[(self.port1, 0), (_ntwk1, 0)],
                       [(_ntwk1, 1), (self.ntwk2, 0)],
                       [(self.ntwk2, 1), (self.port2, 0)]]

        _ntwk1.frequency = rf.Frequency(start=1, stop=1, npoints=1, unit='GHz')
        self.assertRaises(AttributeError, rf.Circuit, connections)

    def test_no_duplicate_node(self):
        """
        Check that a circuit description has no duplicated (network, port)
        """
        # (port1, 0) is found twice in the connections description
        connections = [[(self.port1, 0), (self.ntwk1, 0)],
                       [(self.ntwk1, 1), (self.ntwk2, 0)],
                       [(self.ntwk2, 1), (self.port1, 0)]]
        self.assertRaises(AttributeError, rf.Circuit, connections)

    def test_s_active(self):
        """
        Test the active s-parameter of a 2-ports network
        """
        # s_act should be equal to s11 if a = [1,0]
        assert_array_almost_equal(self.circuit.s_active([1, 0])[:,0], self.circuit.s_external[:,0,0])
        # s_act should be equal to s22 if a = [0,1]
        assert_array_almost_equal(self.circuit.s_active([0, 1])[:,1], self.circuit.s_external[:,1,1])

    def test_auto_reduce(self):
        """
        Test the auto_reduce parameter of the Circuit constructor
        """
        # Explicitly enable the `reduce_circuit` method
        reduced_circuit = rf.Circuit(self.connections, auto_reduce=True)
        assert_array_almost_equal(self.circuit.s_external, reduced_circuit.s_external)
        self.assertNotEqual(self.circuit.dim, reduced_circuit.dim)

        # Implicitly enable the `reduce_circuit` method
        implicit_reduced_circuit = rf.Circuit(self.connections, max_nports=10)
        self.assertEqual(reduced_circuit.dim, implicit_reduced_circuit.dim)

    def test_auto_reduce_with_passed_arguments(self):
        """
        Test the auto_reduce parameter of the Circuit constructor with passed arguments
        """
        # Test with max_nports=1 and dynamic_networks in a tuple or a list
        kwargs = [('max_nports', 1),
                  ('dynamic_networks', (self.ntwk2,)),
                  ('dynamic_networks', [self.ntwk2])]

        # No connections should be reduced
        for key, value in kwargs:
            circuit = rf.Circuit(self.connections, **{key: value})
            assert_array_almost_equal(self.circuit.s_external, circuit.s_external)
            self.assertEqual(circuit.connections, self.connections)

    def test_update_networks(self):
        """
        Test the update_networks method of the Circuit class
        """
        # Create an abnormal circuit with a random network
        ntwk3 = self.ntwk1.copy()
        ntwk3.name = 'ntwk3'
        connections = [[(self.port1, 0), (self.ntwk1, 0)],
                       [(self.ntwk1, 1), (ntwk3, 0)],
                       [(ntwk3, 1), (self.port2, 0)]]
        circuit = rf.Circuit(connections, dynamic_networks=(ntwk3,))

        # should raise an exception if passing a network not in the circuit
        with self.assertRaises(ValueError):
            _ = circuit.update_networks(networks=(self.ntwk2,))

        # Update the circuit with the self.ntwk2
        ntwk3.s = self.ntwk2.s

        # Check the result type and values
        circuit_updated = circuit.update_networks(networks=(ntwk3,))
        self.assertTrue(isinstance(circuit_updated, rf.Circuit))
        assert_array_almost_equal(self.circuit.s_external, circuit_updated.s_external)

        # Check the result type and values when updating the circuit inplace
        none_result = circuit.update_networks(networks=(ntwk3,), inplace=True)
        self.assertIsNone(none_result)
        assert_array_almost_equal(self.circuit.s_external, circuit.s_external)

    def test_cache_attributes(self):
        """
        Test the cached attributes of the Circuit
        """
        init_circuit = rf.Circuit(self.connections)

        cached_attributes = rf.Circuit.CACHEDPROPERTIES

        # Initial circuit should not have cached attributes
        for attr in cached_attributes:
            self.assertFalse(init_circuit.__dict__.get(attr, False))

        # Access the attributes to cache them
        values = [getattr(init_circuit, attr) for attr in cached_attributes]

        # Check that the cached attributes are set correctly
        for attr, value in zip(cached_attributes, values):
            assert_array_almost_equal(init_circuit.__dict__.get(attr, 0.0), value)

        # Modify connections to invalidate the cache
        init_circuit.connections = self.connections
        for attr in cached_attributes:
            self.assertFalse(init_circuit.__dict__.get(attr, False))

    def test_auxiliary_matrix(self):
        """
        Test the auxiliary matrix [T] of the Circuit
        """
        # Calculate the auxiliary matrix T and C @ X
        X, C = self.circuit.X, self.circuit.C
        X_F, C_F = self.circuit.X_F, self.circuit.C_F
        rst, T = - C @ X, self.circuit.T

        # Check that auxiliary_matrices is equal to initial values
        assert_array_almost_equal(T, rst)
        assert_array_almost_equal(X, X_F)
        assert_array_almost_equal(C, C_F)

        # Check the memory layout of the matrices
        self.assertTrue(rst.flags['C_CONTIGUOUS'])
        self.assertTrue(T.flags['F_CONTIGUOUS'])
        self.assertTrue(self.circuit.C.flags['C_CONTIGUOUS'])
        self.assertTrue(self.circuit.C_F.flags['F_CONTIGUOUS'])
        self.assertTrue(self.circuit.X.flags['C_CONTIGUOUS'])
        self.assertTrue(self.circuit.X_F.flags['F_CONTIGUOUS'])

class CircuitClassMethods(unittest.TestCase):
    """
    Test the various class methods of Circuit such as Ground, Port, etc.
    """
    def setUp(self):
        self.freq = rf.Frequency(start=1, stop=2, npoints=101, unit='GHz')
        self.media = rf.DefinedGammaZ0(self.freq)

    def test_ground(self):
        """
        Ground object are infinite shunt admittance (ie. a 2-port short)
        """
        # should raise an exception if no name is passed
        with self.assertRaises(TypeError):
            gnd = rf.Circuit.Ground(self.freq)

        gnd = rf.Circuit.Ground(self.freq, 'gnd')
        gnd_ref = rf.Network(frequency=self.freq,
                             s=np.tile(np.array([[-1,]]),
                                       (len(self.freq),1,1)))

        assert_array_almost_equal(gnd.s, gnd_ref.s)


    def test_open(self):
        """
        Open object are infinite series resistance (ie. a 2-port open)
        """
        # should raise an exception if no name is passed
        with self.assertRaises(TypeError):
            opn = rf.Circuit.Open(self.freq)

        opn = rf.Circuit.Open(self.freq, 'open')
        opn_ref = rf.Network(frequency=self.freq,
                             s=np.tile(np.array([[1]]),
                                       (len(self.freq),1,1)))

        assert_array_almost_equal(opn.s, opn_ref.s)

    def test_series_impedance(self):
        z0s = [1, 50]
        Zs = [1, 1 + 1j, rf.INF]
        for z0 in z0s:
            for Z in Zs:
                assert_array_almost_equal(
                    rf.Circuit.SeriesImpedance(self.freq, Z, 'imp', z0=z0).s,
                    self.media.resistor(Z, z0=z0).s
                    )

        # Z=0 is a thru
        assert_array_almost_equal(
            rf.Circuit.SeriesImpedance(self.freq, Z=0, name='imp').s,
            self.media.thru().s
            )

    def test_shunt_admittance(self):
        z0s = [1, 50]
        Ys = [1, 1 + 1j, rf.INF]
        for z0 in z0s:
            for Y in Ys:
                assert_array_almost_equal(
                    rf.Circuit.ShuntAdmittance(self.freq, Y, 'imp', z0=z0).s,
                    self.media.shunt(self.media.load(rf.zl_2_Gamma0(z0, 1/Y))).s
                    )

        # Y=INF is a a 2-ports short
        assert_array_almost_equal(
            rf.Circuit.ShuntAdmittance(self.freq, rf.INF, 'imp').s,
            self.media.short(nports=2).s
            )

class CircuitTestWilkinson(unittest.TestCase):
    """
    Create a Wilkinson power divider Circuit [#]_ and test the results
    against network.connect.
    The results in [#]_ do not agree due to an error in the formula (3)
    for mismatched intersections.

    References
    ----------
    .. [#] https://en.wikipedia.org/wiki/Wilkinson_power_divider
    .. [#] P. Hallbjörner, Microw. Opt. Technol. Lett. 38, 99 (2003).


    """
    def setUp(self):
        """
        Circuit setup
        """
        self.test_dir = os.path.dirname(os.path.abspath(__file__))+'/'
        self.freq = rf.Frequency(start=1, stop=2, npoints=101, unit='GHz')
        # characteristic impedance of the ports
        Z0_ports = 50

        # resistor
        self.R = 100
        self.line_resistor = rf.media.DefinedGammaZ0(frequency=self.freq, z0=Z0_ports)
        self.resistor = self.line_resistor.resistor(self.R, name='resistor')

        # branches
        Z0_branches = np.sqrt(2)*Z0_ports
        self.line_branches = rf.media.DefinedGammaZ0(frequency=self.freq, z0=Z0_branches)
        self.branch1 = self.line_branches.line(90, unit='deg', name='branch1')
        self.branch2 = self.line_branches.line(90, unit='deg', name='branch2')

        # ports
        port1 = rf.Circuit.Port(self.freq, name='port1', z0=50)
        port2 = rf.Circuit.Port(self.freq, name='port2', z0=50)
        port3 = rf.Circuit.Port(self.freq, name='port3', z0=50)

        # Connection setup
        self.connections = [
                   [(port1, 0), (self.branch1, 0), (self.branch2, 0)],
                   [(port2, 0), (self.branch1, 1), (self.resistor, 0)],
                   [(port3, 0), (self.branch2, 1), (self.resistor, 1)]
                ]

        self.C = rf.Circuit(self.connections)

        # theoretical results from ref P.Hallbjörner (2003)
        self.X1_nn = np.array([1 - np.sqrt(2), -1, -1])/(1 + np.sqrt(2))
        self.X2_nn = np.array([1 - np.sqrt(2), -3 + np.sqrt(2),
                               -1 - np.sqrt(2)])/(3 + np.sqrt(2))

        self.X1_m1 = 2 / (1 + np.sqrt(2))
        self.X1_m2 = np.sqrt(2) / (1 + np.sqrt(2))
        self.X1_m3 = np.sqrt(2) / (1 + np.sqrt(2))

        self.X2_m1 = 4 / (3 + np.sqrt(2))
        self.X2_m2 = 2*np.sqrt(2) / (3 + np.sqrt(2))
        self.X2_m3 = 2 / (3 + np.sqrt(2))

        self.X1 = np.array([[0, self.X1_m2, self.X1_m3],
                            [self.X1_m1, 0, self.X1_m3],
                            [self.X1_m1, self.X1_m2, 0]]) + np.diag(self.X1_nn)
        self.X2 = np.array([[0, self.X2_m2, self.X2_m3],
                            [self.X2_m1, 0, self.X2_m3],
                            [self.X2_m1, self.X2_m2, 0]]) + np.diag(self.X2_nn)


    @unittest.expectedFailure
    def test_sparam_individual_intersection_matrices(self):
        """
        Testing the individual intersection scattering matrices X_k
        The results in [#]_ do not agree due to an error in the formula (3)
        for mismatched intersections.

        References
        ----------
        .. [#] P. Hallbjörner, Microw. Opt. Technol. Lett. 38, 99 (2003).
        """
        np.testing.assert_array_almost_equal(self.C._Xk(self.connections[0])[0], self.X1)
        np.testing.assert_array_almost_equal(self.C._Xk(self.connections[1])[0], self.X2)
        np.testing.assert_array_almost_equal(self.C._Xk(self.connections[2])[0], self.X2)

    @unittest.expectedFailure
    def test_sparam_global_intersection_matrix(self):
        """
        Testing the global intersection scattering matrix
        The results in [#]_ do not agree due to an error in the formula (3)
        for mismatched intersections.

        References
        ----------
        .. [#] P. Hallbjörner, Microw. Opt. Technol. Lett. 38, 99 (2003).
        """
        from scipy.linalg import block_diag
        assert_array_almost_equal(self.C.X[0], block_diag(self.X1, self.X2, self.X2) )

    def test_sparam_circuit(self):
        """
        Testing the external scattering matrix
        """
        S_theoretical = np.array([[0, 1, 1],
                                  [1, 0, 0],
                                  [1, 0, 0]]) * (-1j/np.sqrt(2))

        # extracting the external ports
        S_ext = self.C.s_external

        assert_array_almost_equal(S_ext[0], S_theoretical)

    def test_compare_with_skrf_wilkison(self):
        """
        Create a Wilkinson power divider using skrf usual Network methods.
        """
        z0_port = 50
        # require to create the three tees
        T0 = self.line_branches.splitter(3, z0=z0_port)
        T1 = self.line_branches.splitter(3, z0=z0_port)
        T2 = self.line_branches.splitter(3, z0=z0_port)

        _wilkinson1 = rf.connect(T0, 1, self.branch1, 0)
        _wilkinson2 = rf.connect(_wilkinson1, 2, self.branch2, 0)
        _wilkinson3 = rf.connect(_wilkinson2, 1, T1, 0)
        _wilkinson4 = rf.connect(_wilkinson3, 1, T2, 0)
        _wilkinson5 = rf.connect(_wilkinson4, 1, self.resistor, 0)
        wilkinson = rf.innerconnect(_wilkinson5, 1, 4)

        ntw_C = self.C.network

        assert_array_almost_equal(ntw_C.s, wilkinson.s)
        assert_array_almost_equal(ntw_C.z0, wilkinson.z0)

    def test_compare_with_designer_wilkinson(self):
        """
        Compare the result with ANSYS Designer model

        Built as in https://www.microwaves101.com/encyclopedias/wilkinson-power-splitters
        """
        designer_wilkinson = rf.Network(os.path.join(self.test_dir, 'designer_wilkinson_splitter.s3p'))
        ntw_C = self.C.network

        assert_array_almost_equal(ntw_C.s[0], designer_wilkinson.s[0], decimal=4)

    def test_s_active(self):
        """
        Test the active s-parameter of a 3-ports network
        """
        # s_act should be equal to s11 if a = [1,0,0]
        assert_array_almost_equal(self.C.network.s_active([1, 0, 0])[:,0], self.C.s_external[:,0,0])
        # s_act should be equal to s22 if a = [0,1,0]
        assert_array_almost_equal(self.C.network.s_active([0, 1, 0])[:,1], self.C.s_external[:,1,1])
        # s_act should be equal to s33 if a = [0,0,1]
        assert_array_almost_equal(self.C.network.s_active([0, 0, 1])[:,2], self.C.s_external[:,2,2])

    def test_circuit_reduce_with_split_multi(self):
        """
        Test the reduce_circuit method with split_multi=True.
        """
        ntw_C = self.C.network

        C_reduced = rf.Circuit(self.connections, split_multi=True)
        ntw_C_reduced = C_reduced.network

        self.assertNotEqual(self.C.dim, C_reduced.dim)
        assert_array_almost_equal(ntw_C.s, ntw_C_reduced.s)

class CircuitTestCascadeNetworks(unittest.TestCase):
    """
    Build a circuit made of two Networks cascaded and compare the result
    to usual cascading of two networks.
    """
    def setUp(self):
        # Importing network examples
        self.test_dir = os.path.dirname(os.path.abspath(__file__))+'/'
        self.ntwk1 = rf.Network(os.path.join(self.test_dir, 'ntwk1.s2p'))
        self.ntwk2 = rf.Network(os.path.join(self.test_dir, 'ntwk2.s2p'))
        # ntwk3 is the cascade of ntwk1 and ntwk2, ie. self.ntwk1 ** self.ntwk2
        self.ntwk3 = rf.Network(os.path.join(self.test_dir, 'ntwk3.s2p'))
        self.freq = self.ntwk1.frequency
        # circuit external ports
        self.port1 = rf.Circuit.Port(self.freq, name='Port1')
        self.port2 = rf.Circuit.Port(self.freq, name='Port2')
        self.open = rf.Circuit.Open(self.freq, name='PortO')
        _media = rf.media.DefinedGammaZ0(frequency=self.freq)
        self.match = _media.match(name='match')
        self.tee = _media.tee(name='tee')
        self.thru = _media.thru()

    def test_cascade(self):
        """
        Compare ntwk3 to the Circuit of ntwk1 and ntwk2.
        """
        connections = [  [(self.port1, 0), (self.ntwk1, 0)],
                         [(self.ntwk1, 1), (self.ntwk2, 0)],
                         [(self.ntwk2, 1), (self.port2, 0)] ]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(circuit.s_external, self.ntwk3.s)

    def test_cascade2(self):
        """
        Same thing with different ordering of the connections.
        Demonstrate that changing the connections setup order does not change
        the result.
        """
        connections = [  [(self.port1, 0), (self.ntwk1, 0)],
                         [(self.ntwk2, 0), (self.ntwk1, 1)],
                         [(self.port2, 0), (self.ntwk2, 1)] ]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(circuit.s_external, self.ntwk3.s)

    def test_cascade3(self):
        """
        Inverting the cascading network order
        Demonstrate that changing the connections setup order does not change
        the result (at the requirement that port impedance are the same).
        """
        connections = [  [(self.port1, 0), (self.ntwk2, 0)],
                         [(self.ntwk2, 1), (self.ntwk1, 0)],
                         [(self.port2, 0), (self.ntwk1, 1)] ]
        circuit = rf.Circuit(connections)
        ntw = self.ntwk2 ** self.ntwk1
        assert_array_almost_equal(circuit.s_external, ntw.s)

    def test_numerical_singular_corner_case(self):
        """
        Test a numerical singular corner case which 2 Tee Networks are cascaded.
        Schematic of the test circuit:
           +------+    +------+          +------------+            +------------+
          -|0     |    |     1|-      p1-|T1(0)  T2(1)|-p3      p1-|T1(0)  T2(1)|-p2
           |     2|----|0     |   =>     |            |     =>  +--|T1(1)  T2(2)|--+
          -|1     |    |     2|-      p2-|T1(1)  T2(2)|-p4      |  +------------+  |
           +------+    +------+          +------------+         +------------------+
              T1          T2                  Temp                      Thru
        """
        T1, T2 = self.tee, self.tee
        temp = rf.connect(T1, 2, T2, 0)

        # Check the s-parameters of the temporary Network
        assert_array_almost_equal(temp.s, np.array([ [ [-0.5,  0.5,  0.5,  0.5],
                                                       [ 0.5, -0.5,  0.5,  0.5],
                                                       [ 0.5,  0.5, -0.5,  0.5],
                                                       [ 0.5,  0.5,  0.5, -0.5], ]
                                                    for _ in range(self.tee.frequency.npoints) ]
                                                    ,dtype=complex))

        connections = [  [(self.port1, 0), (temp, 0)],
                         [(temp, 1),       (temp, 3)],
                         [(self.port2, 0), (temp, 2)] ]

        # Check the s-parameters of the Circuit, the Network should be equal to the thru Network
        # RuntimeWarning not triggered after the PR #1227
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(circuit.network.s, self.thru.s)

    def test_open_condensation(self):
        """
        Test the impact of omitting an open termination in the circuit connections.
        Compare the result of a conventional circuit setup with an open termination
        to a condensed setup that assumes an open termination by default.
        """
        # Conventional setup including open termination
        cnx_con = [  [(self.port1, 0), (self.ntwk1, 0)],
                     [(self.ntwk1, 1), (self.ntwk2, 0), (self.port2, 0)],
                     [(self.ntwk2, 1), (self.open, 0)] ]
        ckt_con = rf.Circuit(cnx_con)

        # Condensed setup where open termination is implied
        cnx_cds = [  [(self.port1, 0), (self.ntwk1, 0)],
                     [(self.ntwk1, 1), (self.ntwk2, 0), (self.port2, 0)],
                     [(self.ntwk2, 1)] # Open termination is implied
                     ]

        ckt_cds = rf.Circuit(cnx_cds)

        assert_array_almost_equal(ckt_con.s_external, ckt_cds.s_external)

    def test_match_condensation(self):
        """
        Test the impact of omitting a matching network in the circuit connections.
        Compare the result of a conventional circuit setup with a matching network
        to a condensed setup that assumes the matching network by default.
        """
        # Conventional setup including matching network
        cnx_con = [  [(self.port1, 0), (self.ntwk1, 0)],
                     [(self.ntwk1, 1), (self.ntwk2, 0), (self.port2, 0)],
                     [(self.ntwk2, 1), (self.match, 0)] ]
        ckt_con = rf.Circuit(cnx_con)

        # Condensed setup where matching network is implied
        cnx_cds = [  [(self.port1, 0), (self.ntwk1, 0)],
                     [(self.ntwk1, 1), (self.ntwk2, 0), (self.port2, 0)]
                     ]

        ckt_cds = rf.Circuit(cnx_cds)

        assert_array_almost_equal(ckt_con.s_external, ckt_cds.s_external)

class CircuitTestMultiPortCascadeNetworks(unittest.TestCase):
    """
    Various 1-ports, 2-ports and 4-ports circuits and associated tests
    """
    def test_1port_matched_load(self):
        """
        Connect a matched load directly to the port
        """
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        port1 = rf.Circuit.Port(freq,  name='port1')
        line = rf.media.DefinedGammaZ0(frequency=freq)
        match_load = line.match(name='match_load')

        cnx = [
            [(port1, 0), (match_load, 0)]
        ]
        cir = rf.Circuit(cnx)

        assert_array_almost_equal(match_load.s, cir.s_external)

    def test_1port_short(self):
        """
        Connect a short directly to the port
        """
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        port1 = rf.Circuit.Port(freq,  name='port1')
        line = rf.media.DefinedGammaZ0(frequency=freq)
        short = line.short(name='short')
        gnd1 = rf.Circuit.Ground(freq, name='gnd')
        # method 1 : use the Ground Network (which 2 port actually)
        cnx = [
            [(port1, 0), (gnd1, 0)]
        ]
        cir = rf.Circuit(cnx)
        assert_array_almost_equal(short.s, cir.s_external)
        # method 2 : use a short Network (1 port)
        cnx = [
            [(port1, 0), (short, 0)]
        ]
        cir = rf.Circuit(cnx)

        assert_array_almost_equal(short.s, cir.s_external)

    def test_1port_random_load(self):
        """
        Connect a random load directly to the port
        """
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        port1 = rf.Circuit.Port(freq,  name='port1')
        line = rf.media.DefinedGammaZ0(frequency=freq)
        gamma = np.random.default_rng().random((1,1)) + 1j*np.random.default_rng().random((1,1))
        load = line.load(gamma, name='load')

        cnx = [
            [(port1, 0), (load, 0)]
        ]
        cir = rf.Circuit(cnx)

        assert_array_almost_equal(load.s, cir.s_external)

    def test_1port_matched_network_default_impedance(self):
        """
        Connect a random 2 port network connected to a matched load
        """
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(4).reshape(2, 2)
        line = rf.media.DefinedGammaZ0(frequency=freq)
        match_load = line.match(name='match_load')

        # classic connecting
        b = a ** match_load

        # Circuit connecting
        port1 = rf.Circuit.Port(freq,  name='port1')
        connections = [[(port1, 0), (a, 0)], [(a, 1), (match_load, 0)]]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(b.s, circuit.s_external)

    def test_1port_matched_network_complex_impedance(self):
        """
        Connect a 2 port network to a complex impedance.
        Both ports are complex.
        """
        z01, z02 = 1-1j, 2+4j
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(4).reshape(2, 2)
        a.z0 = [z01, z02]
        line = rf.media.DefinedGammaZ0(frequency=freq, z0=z02)
        match_load = line.match(name='match_load')

        # classic connecting
        b = a ** match_load

        # Circuit connecting
        port1 = rf.Circuit.Port(freq, z0=z01, name='port1')
        connections = [[(port1, 0), (a,0)], [(a, 1), (match_load, 0)]]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(b.s, circuit.s_external)

    def test_2ports_default_characteristic_impedance(self):
        """
        Connect two 2-ports networks in a resulting  2-ports network,
        same default charact impedance (50 Ohm) for all ports
        """
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(4).reshape(2, 2)

        b = rf.Network(name='b')
        b.frequency = freq
        b.s = np.random.default_rng().random(4).reshape(2, 2)

        # classic connecting
        c = rf.connect(a, 1, b, 0)

        # Circuit connecting
        port1 = rf.Circuit.Port(freq,  name='port1')
        port2 = rf.Circuit.Port(freq,  name='port2')

        connections = [[(port1, 0), (a, 0)],
                       [(a, 1), (b, 0)],
                       [(b, 1), (port2, 0)]]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(c.s, circuit.s_external)

    def test_2ports_complex_characteristic_impedance(self):
        """
        Connect two 2-ports networks in a resulting  2-ports network,
        same complex charact impedance (1+1j) for all ports
        """
        z0 = 1 + 1j
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(4).reshape(2, 2)
        a.z0 = z0

        b = rf.Network(name='b')
        b.frequency = freq
        b.s = np.random.default_rng().random(4).reshape(2, 2)
        b.z0 = z0

        # classic connecting
        c = rf.connect(a, 1, b, 0)

        # Circuit connecting
        port1 = rf.Circuit.Port(freq, z0=z0, name='port1')
        port2 = rf.Circuit.Port(freq, z0=z0, name='port2')

        connections = [[(port1, 0), (a, 0)],
                       [(a, 1), (b, 0)],
                       [(b, 1), (port2, 0)]]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(c.s, circuit.s_external)

    def test_2ports_different_characteristic_impedances(self):
        """
        Connect two 2-ports networks in a resulting  2-ports network,
        different characteristic impedances for each network ports
        """
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(4).reshape(2,2)
        a.z0 = [1, 2]  #  Z0 should never be zero

        b = rf.Network(name='b')
        b.frequency = freq
        b.s = np.random.default_rng().random(4).reshape(2,2)
        b.z0 = [11, 12]

        # classic connecting
        c = rf.connect(a, 1, b, 0)

        # Circuit connecting
        port1 = rf.Circuit.Port(freq, z0=1, name='port1')
        port2 = rf.Circuit.Port(freq, z0=12, name='port2')

        connections = [[(port1, 0), (a, 0)],
                       [(a, 1), (b, 0)],
                       [(b, 1), (port2, 0)] ]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(c.s, circuit.s_external)

    def test_4ports_default_characteristic_impedances(self):
        """
        Connect two 4-ports networks in a resulting 4-ports network,
        with default characteristic impedances
        """
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(16).reshape(4, 4)

        b = rf.Network(name='b')
        b.frequency = freq
        b.s = np.random.default_rng().random(16).reshape(4, 4)

        # classic connecting
        c = rf.connect(a, 2, b, 0, 2)

        # Circuit connecting
        port1 = rf.Circuit.Port(freq, name='port1')
        port2 = rf.Circuit.Port(freq, name='port2')
        port3 = rf.Circuit.Port(freq, name='port3')
        port4 = rf.Circuit.Port(freq, name='port4')

        connections = [[(port1, 0), (a, 0)],
                       [(port2, 0), (a, 1)],
                       [(a, 2), (b, 0)],
                       [(a, 3), (b, 1)],
                       [(b, 2), (port3, 0)],
                       [(b, 3), (port4, 0)]]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(c.s, circuit.s_external)

    def test_4ports_complex_characteristic_impedances(self):
        """
        Connect two 4-ports networks in a resulting 4-ports network,
        with same complex characteristic impedances
        """
        z0 = 5 + 4j
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(16).reshape(4, 4)
        a.z0 = z0

        b = rf.Network(name='b')
        b.frequency = freq
        b.s = np.random.default_rng().random(16).reshape(4, 4)
        b.z0 = z0

        # classic connecting
        c = rf.connect(a, 2, b, 0, 2)

        # circuit connecting
        port1 = rf.Circuit.Port(freq, z0=z0, name='port1')
        port2 = rf.Circuit.Port(freq, z0=z0, name='port2')
        port3 = rf.Circuit.Port(freq, z0=z0, name='port3')
        port4 = rf.Circuit.Port(freq, z0=z0, name='port4')

        connections = [ [(port1, 0), (a, 0)],
                        [(port2, 0), (a, 1)],
                        [(a, 2), (b, 0)],
                        [(a, 3), (b, 1)],
                        [(b, 2), (port3, 0)],
                        [(b, 3), (port4, 0)]]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(c.s, circuit.s_external)

    def test_4ports_different_characteristic_impedances(self):
        """
        Connect two 4-ports networks in a resulting 4-ports network,
        with different characteristic impedances
        """
        z0 = [1, 2, 3, 4]
        freq = rf.Frequency(start=1, npoints=1, unit='GHz')
        a = rf.Network(name='a')
        a.frequency = freq
        a.s = np.random.default_rng().random(16).reshape(4, 4)
        a.z0 = z0

        b = rf.Network(name='b')
        b.frequency = freq
        b.s = np.random.default_rng().random(16).reshape(4, 4)
        b.z0 = [11, 12, 13, 14]

        # classic connecting
        _c = rf.connect(a, 2, b, 0)
        c = rf.innerconnect(_c, 2, 3)

        # Circuit connecting
        port1 = rf.Circuit.Port(freq, z0=1, name='port1')
        port2 = rf.Circuit.Port(freq, z0=2, name='port2')
        port3 = rf.Circuit.Port(freq, z0=13, name='port3')
        port4 = rf.Circuit.Port(freq, z0=14, name='port4')

        connections = [[(port1, 0), (a, 0)],
                       [(port2, 0), (a, 1)],
                       [(a, 2), (b, 0)],
                       [(a, 3), (b, 1)],
                       [(b, 2), (port3, 0)],
                       [(b, 3), (port4, 0)]]
        circuit = rf.Circuit(connections)

        assert_array_almost_equal(c.s, circuit.s_external)

    def test_shunt_element(self):
        """
        Compare a shunt element network (here a capacitor)
        """
        freq = rf.Frequency(start=1, stop=2, npoints=101, unit='GHz')
        line = rf.media.DefinedGammaZ0(frequency=freq, z0=50)
        # usual way
        cap_shunt_manual = line.shunt_capacitor(50e-12)

        # A Circuit way
        port1 = rf.Circuit.Port(frequency=freq, name='port1', z0=50)
        port2 = rf.Circuit.Port(frequency=freq, name='port2', z0=50)
        cap_shunt = line.capacitor(50e-12, name='cap_shunt')
        ground = rf.Circuit.Ground(frequency=freq, name='ground', z0=50)

        connections = [
            [(port1, 0), (cap_shunt, 0), (port2, 0)],
            [(cap_shunt, 1), (ground, 0)]
        ]

        # # Another possibility could have been without ground :
        # shunt_cap = line.shunt_capacitor(50e-12)
        # shunt_cap.name='shunt_cap'
        # connections = [
        #     [(port1, 0), (shunt_cap, 0)],
        #     [(shunt_cap ,1), (port2, 0)]
        # ]

        cap_shunt_from_circuit = rf.Circuit(connections).network

        assert_array_almost_equal(cap_shunt_manual.s, cap_shunt_from_circuit.s)

    def test_reduce_circuit(self):
        """
        Test reducing multi-port cascade circuit
        """
        freq_n = 101
        freq = rf.Frequency(start=1, stop=2, npoints=freq_n, unit='GHz')
        line = rf.media.DefinedGammaZ0(frequency=freq, z0=50)

        # network A
        ntwkA = rf.Network(name='a')
        ntwkA.frequency = freq
        ntwkA_z0 = (60, 70, 80, 90, 100)
        ntwkA_np = len(ntwkA_z0)
        ntwkA.z0 = [ntwkA_z0]*freq_n
        ntwkA.s = np.random.default_rng().random(freq_n * ntwkA_np**2).reshape(freq_n, ntwkA_np, ntwkA_np)

        # network B
        ntwkB = rf.Network(name='b')
        ntwkB.frequency = freq
        ntwkB_z0 = (10, 20, 30, 40, 50)
        ntwkB_np = len(ntwkB_z0)
        ntwkB.z0 = [ntwkB_z0]*freq_n
        ntwkB.s = np.random.default_rng().random(freq_n * ntwkB_np**2).reshape(freq_n, ntwkB_np, ntwkB_np)

        # Construct the connection
        port1 = rf.Circuit.Port(frequency=freq, name='port1', z0=50)
        port2 = rf.Circuit.Port(frequency=freq, name='port2', z0=50)
        ground1 = rf.Circuit.Ground(frequency=freq, name='ground1', z0=50)
        ground2 = rf.Circuit.Ground(frequency=freq, name='ground2', z0=50)
        open_port = rf.Circuit.Open(frequency=freq, name='open_port', z0=50)
        ind_shunt = line.inductor(50e-12, name='ind_shunt')
        cap_series = line.capacitor(30e-9, name='cap_series')
        port3 = rf.Circuit.Port(frequency=freq, name='port3', z0=50)
        port4 = rf.Circuit.Port(frequency=freq, name='port4', z0=50)

        connections = [
            [(ntwkA, 0), (cap_series, 0)],
            [(port1, 0), (ind_shunt, 0), (cap_series, 1)],
            [(ground1, 0), (ind_shunt, 1)],
            [(ntwkA, 1), (port2, 0)],
            [(ntwkA, 2), (ground2, 0)],
            [(ntwkA, 3), (open_port, 0)],
            [(ntwkA, 4), (ntwkB, 0)],
            [(ntwkB, 1), (port3, 0)],
            [(ntwkB, 2), (port4, 0)],
            [(ntwkB, 3), (ntwkB, 4)],
        ]

        # Circuit connecting
        circuit = rf.Circuit(connections)

        # Reduce the circuit
        reduced_cnxs = rf.reduce_circuit(connections)
        reduced_circuit = rf.Circuit(reduced_cnxs)

        assert_array_almost_equal(circuit.network.s, reduced_circuit.network.s)

class CircuitTestVariableCoupler(unittest.TestCase):
    """
    If we use 3 dB hybrid defined as :
                   ________
    Input     0 --|       |-- 1 Through
                  |       |
    Isolated  3 --|_______|-- 2 Coupled

    And if we connect two 3 dB hybrid with a phase shifter such as:
                 _________                                 _________
    Port#3 >- 0-|        |-1 -------------------------- 0-|        |-1 -< Port#2
                |  Hyb1  |                                |  Hyb2  |
    Port#0 >- 3-|________|-2 -- 0-[phase shifter]-1 -- 3-|________|-2 -< Port#1

    Then we have a variable coupler. The coupling factor can be adjusted
    by changing the phase of the phase shifter.

    The port order in this example is voluntary complicated to make a good
    example.

    """
    def setUp(self):
        self.freq = rf.Frequency(start=1.5, stop=1.5, npoints=1, unit='GHz')
        self.coax = rf.media.DefinedGammaZ0(frequency=self.freq)
        self.test_dir = os.path.dirname(os.path.abspath(__file__))+'/'

    def phase_shifter(self, phase_deg):
        return self.coax.line(d=phase_deg, unit='deg')

    def hybrid(self, name='hybrid'):
        Sc = 1/np.sqrt(2)*np.array([[ 0,  1, 1j,  0],
                                    [ 1,  0,  0, 1j],
                                    [1j,  0,  0,  1],
                                    [ 0, 1j,  1,  0]])
        hybrid = rf.Network(frequency=self.freq, s=Sc, name=name)
        return hybrid

    def variable_coupler_network_from_connect(self, phase_deg):
        ps = self.phase_shifter(phase_deg)
        hybrid1, hybrid2 = self.hybrid(), self.hybrid()
        # connecting the networks together.
        # NB: in order to know which port corresponds to which, it is useful
        # to define different port characteristic impedances like
        # hybrid1.z0 = [11, 12, 13, 14]
        # hybrid2.z0 = [21, 22, 23, 24]
        # ps.z0 = [31, 32]
        # and to make a drawing of each steps.
        # This is not convenient, that's why the Circuit approach can be easier.
        _temp = rf.connect(hybrid1, 2, ps, 0)
        _temp = rf.connect(_temp, 1, hybrid2, 0)
        _temp = rf.innerconnect(_temp, 1, 5)
        # re-order port numbers to match the example
        _temp.renumber([0, 1, 2, 3], [3, 0, 2, 1])
        return _temp

    def variable_coupler_circuit(self, phase_deg):
        ps = self.phase_shifter(phase_deg)
        ps.name = 'ps'  # do not forget the name of the network !
        hybrid1, hybrid2 = self.hybrid('hybrid1'), self.hybrid('hybrid2')

        port1 = rf.Circuit.Port(ps.frequency, 'port1')
        port2 = rf.Circuit.Port(ps.frequency, 'port2')
        port3 = rf.Circuit.Port(ps.frequency, 'port3')
        port4 = rf.Circuit.Port(ps.frequency, 'port4')
        # Note that the order of port appearance is important.
        # 1st port to appear in the connection setup will be the 1st port (0),
        # then second to appear the second port (1), etc...
        # There is no constraint for the order of the connections.
        connections = [
                       [(port1, 0), (hybrid1, 3)],
                       [(port2, 0), (hybrid2, 2)],
                       [(port3, 0), (hybrid2, 1)],
                       [(port4, 0), (hybrid1, 0)],
                       [(hybrid1, 2), (ps, 0)],
                       [(hybrid1, 1), (hybrid2, 0)],
                       [(ps, 1), (hybrid2, 3)],
                      ]

        return rf.Circuit(connections)

    def variable_coupler_network_from_circuit(self, phase_deg):
        return self.variable_coupler_circuit(phase_deg).network

    def test_compare_with_network_connect(self):
        """
        Compare with the S-parameters obtained from Network.connect
        """
        phase_deg = np.random.default_rng().integers(low=0, high=180)
        vc_connect = self.variable_coupler_network_from_connect(phase_deg)
        vc_circuit = self.variable_coupler_network_from_circuit(phase_deg)
        assert_array_almost_equal(vc_connect.s, vc_circuit.s)

    def test_compare_with_designer(self):
        """
        Compare with the S-parameters obtained from ANSYS Designer
        """
        for phase_angle in [20, 75]:
            vc_designer = rf.Network(os.path.join(self.test_dir,
                                                  f"designer_variable_coupler_ideal_{phase_angle}deg.s4p"))
            vc_circuit = self.variable_coupler_network_from_circuit(phase_angle)
            assert_array_almost_equal(vc_designer.s, vc_circuit.s, decimal=4)

    def test_compare_connect_and_designer(self):
        """
        Compare S-parameters obtained from ANSYS Designer with Network.connect
        """
        for phase_angle in [20, 75]:
            vc_designer = rf.Network(os.path.join(self.test_dir,
                                                  f"designer_variable_coupler_ideal_{phase_angle}deg.s4p"))
            vc_connect = self.variable_coupler_network_from_connect(phase_angle)
            assert_array_almost_equal(vc_designer.s, vc_connect.s, decimal=4)


class CircuitTestGraph(unittest.TestCase):
    """
    Test functionalities linked to graph method, used in particular for plotting
    """

    def setUp(self):
        """
        Dummy Circuit setup

        Setup a circuit which has various interconnections (2 or 3)
        """
        self.freq = rf.Frequency(start=1, stop=2, npoints=101, unit='GHz')

        # dummy components
        self.R = 100
        self.line_resistor = rf.media.DefinedGammaZ0(frequency=self.freq, z0=self.R)
        resistor1 = self.line_resistor.resistor(self.R, name='resistor1')
        resistor2 = self.line_resistor.resistor(self.R, name='resistor2')
        resistor3 = self.line_resistor.resistor(self.R, name='resistor3')
        port1 = rf.Circuit.Port(self.freq, name='port1')

        # Connection setup
        self.connections = [
                   [(port1, 0), (resistor1, 0), (resistor3, 0)],
                   [(resistor1, 1), (resistor2, 0)],
                   [(resistor2, 1), (resistor3, 1)]
                ]

        self.C = rf.Circuit(self.connections)

    def test_intersection_dict(self):
        inter_dict = self.C.intersections_dict
        # should have 3 intersections
        self.assertTrue(len(inter_dict) == 3)
        # All intersections should have at least 2 edges
        for it in inter_dict.items():
            k, cnx = it
            self.assertTrue(len(cnx) >= 2)

    def test_edge_labels(self):
        edge_labels = self.C.edge_labels
        self.assertTrue(len(edge_labels) == 7)


class CircuitTestComplexCharacteristicImpedance(unittest.TestCase):
    """
    Test creating circuits with real and complex port charac.impedances
    """
    def setUp(self):
        self.f0 = rf.Frequency(75.8, npoints=1, unit='GHz')
        # initial s-param values of A 2 ports network
        self.s0 = np.array([  # dummy values
            [-0.1000 -0.2000j, -0.3000 +0.4000j],
            [-0.3000 +0.4000j, 0.5000 -0.6000j]]).reshape(-1,2,2)

        # build initial network (under z0=50)
        self.ntw0 = rf.Network(frequency=self.f0, s=self.s0, z0=50, name='dut')

        # complex characteristic impedance to renormalize to
        self.zdut = 100 + 10j

        # reference solutions obtained from ANSYS Circuit or ADS (same res)
        # when z0=[50, zdut]
        self.z_ref = np.array([  # not affected by z0
            [18.0000 -16.0000j, 20.0000 +40.0000j],
            [20.0000 +40.0000j, 10.0000 -80.0000j]]).reshape(-1,2,2)

        self.y_ref = np.array([  # not affected by z0
            [0.0251 +0.0023j, 0.0123 -0.0066j],
            [0.0123 -0.0066j, 0.0052 +0.0055j]]).reshape(-1,2,2)

        self.s_ref = np.array([  # renormalized s (power-waves)
            [-0.1374 -0.2957j, -0.1995 +0.5340j],
            [-0.1995 +0.5340j, -0.0464 -0.7006j]]).reshape(-1,2,2)

        # Creating equivalent reference circuit
        port1 = rf.Circuit.Port(self.f0, z0=50, name='port1')
        port2 = rf.Circuit.Port(self.f0, z0=50, name='port2')
        ntw0 = self.ntw0

        cnx = [  # z0=[50,50]
            [(port1, 0), (ntw0, 0)],
            [(ntw0, 1), (port2, 0)]
            ]
        self.cir = rf.Circuit(cnx)

        # Creating equivalent circuit with z0 real
        port2_real = rf.Circuit.Port(self.f0, z0=100, name='port2')
        cnx_real = [  # z0=[50,100]
            [(port1, 0), (ntw0, 0)],
            [(ntw0, 1), (port2_real, 0)]
            ]
        self.cir_real = rf.Circuit(cnx_real)

        # Creating equivalent circuit with z0 complex
        port2_complex = rf.Circuit.Port(self.f0, z0=self.zdut, name='port2')
        cnx_complex = [  # z0=[50,zdut]
            [(port1, 0), (ntw0, 0)],
            [(ntw0, 1), (port2_complex, 0)]
            ]
        self.cir_complex = rf.Circuit(cnx_complex)

        # references for each s-param definition
        self.s_legacy = rf.renormalize_s(self.s0, [50,50], [50,self.zdut],
                                         s_def='traveling')
        self.s_power = rf.renormalize_s(self.s0, [50,50], [50,self.zdut],
                                         s_def='power')
        self.s_pseudo = rf.renormalize_s(self.s0, [50,50], [50,self.zdut],
                                         s_def='pseudo')
        # for real values, should be = whatever s-param definition
        self.s_real = rf.renormalize_s(self.s0, [50,50], [50,100])

    def test_verify_reference(self):
        ' Check that the reference results comes from power-waves definition'
        np.testing.assert_allclose(self.s_ref, self.s_power, atol=1e-4)

    def test_reference_s(self):
        ' Check reference z0 circuit, just in case '
        np.testing.assert_allclose(self.cir.network.s, self.s0, atol=1e-4)

    def test_real_z0_s(self):
        ' Check real z0 circuit '
        np.testing.assert_allclose(self.cir_real.network.s, self.s_real, atol=1e-4)

    def test_real_z_params(self):
        ' Check Z-parameters match'
        np.testing.assert_allclose(self.cir.network.z, self.cir_real.network.z, atol=1e-4)

    def test_complex_z_params(self):
        ' Check Z-parameters match'
        np.testing.assert_allclose(self.cir_complex.network.z, self.cir_real.network.z, atol=1e-4)

    @unittest.expectedFailure
    def test_complexz0_s_vs_legacy(self):
        ' Check complex z0 circuit vs legacy renormalization '
        np.testing.assert_allclose(self.cir_complex.network.s, self.s_legacy, atol=1e-4)

    def test_complexz0_s_vs_powerwaves(self):
        ' Check complex z0 circuit vs power-waves renormalization '
        np.testing.assert_allclose(self.cir_complex.network.s, self.s_ref, atol=1e-4)
        np.testing.assert_allclose(self.cir_complex.network.s, self.s_power, atol=1e-4)

    @unittest.expectedFailure
    def test_complexz0_s_vs_pseudo(self):
        ' Check complex z0 circuit vs pseudo-waves renormalization '
        np.testing.assert_allclose(self.cir_complex.network.s, self.s_pseudo, atol=1e-4)

class CircuitTestVoltagesCurrents(unittest.TestCase):
    def setUp(self):
        rng = np.random.default_rng()
        # setup a test transmission line randomly excited
        self.P_f = rng.random()  # forward power in Watt
        self.phase_f = rng.random()  # forward phase in rad
        self.Z = rng.random()  # source internal impedance, line characteristic impedance and load impedance
        self.L = rng.random()  # line length in [m]
        self.L2 = rng.random()  # line length in [m]
        self.L3 = rng.random()  # line length in [m]
        self.freq = rf.Frequency(1, 10, 10, unit='GHz')
        self.line_media = rf.media.DefinedGammaZ0(self.freq, z0=self.Z)  # lossless line medium
        self.line = self.line_media.line(d=self.L, unit='m', name='line')  # transmission line Network
        self.line2 = self.line_media.line(d=self.L2, unit='m', name='line2')  # transmission line Network
        self.line3 = self.line_media.line(d=self.L3, unit='m', name='line3')  # transmission line Network
        self.tee = self.line_media.tee(name='tee')
        self.resistor = self.line_media.resistor(50, name='resistor')

        # forward voltages and currents at the input of the test line
        self.V_in = np.sqrt(2*self.Z*self.P_f)*np.exp(1j*self.phase_f)
        self.I_in = np.sqrt(2*self.P_f/self.Z)*np.exp(1j*self.phase_f)
        # forward voltages and currents at the output of the test line
        theta = rf.theta(self.line_media.gamma, self.freq.f, self.L)  # electrical length
        self.V_out, self.I_out = rf.tlineFunctions.voltage_current_propagation(self.V_in, self.I_in, self.Z, theta)

        # Equivalent model with Circuit
        self.port1 = rf.Circuit.Port(frequency=self.freq, name='port1', z0=self.Z)
        self.port2 = rf.Circuit.Port(frequency=self.freq, name='port2', z0=self.Z)
        cnx = [
            [(self.port1, 0), (self.line, 0)],
            [(self.port2, 0), (self.line, 1)]
        ]
        self.crt = rf.Circuit(cnx)
        # power and phase arrays for Circuit.voltages() and currents()
        self.power = [self.P_f, 0]
        self.phase = [self.phase_f, 0]

    def test_tline_voltages(self):
        ' Test voltages for a simple transmission line '
        V_ports = self.crt.voltages_external(self.power, self.phase)

        np.testing.assert_allclose(self.V_in, V_ports[:,0])
        np.testing.assert_allclose(self.V_out, V_ports[:,1])

    def test_tline_currents(self):
        ' Test currents for a simple transmission line '
        I_ports = self.crt.currents_external(self.power, self.phase)

        np.testing.assert_allclose(self.I_in, I_ports[:,0])
        # output current is * -1 as Circuit definition is opposite
        # (toward the Circuit's Port)
        np.testing.assert_allclose(self.I_out, -1*I_ports[:,1])

    def test_multiple_ports(self):
        ' Test voltages and currents for a connection with multiple ports '
        # Create a circuit build with tee
        cnx_with_tee = [
            [(self.port1, 0), (self.line, 0)],
            [(self.line, 1), (self.tee, 0)],
            [(self.line2, 0), (self.tee, 1)],
            [(self.line3, 0), (self.tee, 2)],
            [(self.line2, 1), (self.port2, 0)],
            [(self.line3, 1), (self.resistor, 0)],
        ]

        # Create a circuit build without tee
        cnx = [
            [(self.port1, 0), (self.line, 0)],
            [(self.line, 1), (self.line2, 0), (self.line3, 0)],
            [(self.line2, 1), (self.port2, 0)],
            [(self.line3, 1), (self.resistor, 0)],
        ]

        # Create the two circuits
        crt_with_tee = rf.Circuit(cnx_with_tee)
        crt = rf.Circuit(cnx)

        # Get voltages and currents for both circuits
        V_with_tee = crt_with_tee.voltages(self.power, self.phase)
        I_with_tee = crt_with_tee.currents(self.power, self.phase)

        V = crt.voltages(self.power, self.phase)
        I = crt.currents(self.power, self.phase)

        # Get the reordering of the ports in the two circuits
        port_order_with_tee = (0, 1, 2, 4, 6, 8, 9, 10, 11)
        port_order = (0, 1, 2, 3, 4, 5, 6, 7, 8)

        for v, v_with_tee in zip((V, I), (V_with_tee, I_with_tee)):
            v_with_tee_reordered = np.array(
                [v_with_tee[:, i] for i in port_order_with_tee]
            )
            v_reordered = np.array([v[:, i] for i in port_order])

            np.testing.assert_allclose(v_with_tee_reordered, v_reordered)

    def test_tline_with_different_impedance(self):
        ' Test voltages and currents for a simple transmission line with different impedances '
        line = self.line.copy()
        line.renormalize(z_new=1./self.Z)

        # Equivalent model with Circuit
        cnx = [
            [(self.port1, 0), (line, 0)],
            [(self.port2, 0), (line, 1)]
        ]
        crt = rf.Circuit(cnx)

        V_ports_uni_z = crt.voltages(self.power, self.phase)
        I_ports_uni_z = crt.currents(self.power, self.phase)

        V_ports_dif_z = self.crt.voltages(self.power, self.phase)
        I_ports_dif_z = self.crt.currents(self.power, self.phase)

        np.testing.assert_allclose(I_ports_uni_z, I_ports_dif_z)
        np.testing.assert_allclose(V_ports_uni_z, V_ports_dif_z)

class CircuitTestVoltagesNonReciprocal(unittest.TestCase):
    def test_isolator(self):
        # Isolator that passes from port 1 to 2
        freq = rf.Frequency.from_f([1], unit='GHz')
        s = np.array([[[0, 0], [1, 0]]])
        network = rf.Network(frequency=freq, s=s, name='isolator')
        port1 = rf.Circuit.Port(frequency=freq, name='port1', z0=50)
        port2 = rf.Circuit.Port(frequency=freq, name='port2', z0=50)
        cnx = [
            [(network,0),(port1,0)],
            [(network,1),(port2,0)]
        ]
        crt = rf.Circuit(cnx)
        np.testing.assert_allclose(crt.s_external, s)

        power = [1,0] # 1 Watt at port 1
        phase = [0,0]
        V_at_ports = crt.voltages_external(power, phase)
        I_at_ports = crt.currents_external(power, phase)
        np.testing.assert_allclose(V_at_ports, [[10+0j, 10+0j]])
        # Positive current entering into port
        np.testing.assert_allclose(I_at_ports, [[0.2+0j, -0.2+0j]])

        power = [0,1] # 1 Watt at port 2
        V_at_ports = crt.voltages_external(power, phase)
        I_at_ports = crt.currents_external(power, phase)
        np.testing.assert_allclose(V_at_ports, [[0+0j, 10+0j]])
        np.testing.assert_allclose(I_at_ports, [[0+0j, 0.2+0j]])

    def test_isolator_reverse(self):
        # Isolator that passes from port 2 to 1
        freq = rf.Frequency.from_f([1], unit='GHz')
        s = np.array([[[0, 1], [0, 0]]])
        network = rf.Network(frequency=freq, s=s, name='isolator')
        port1 = rf.Circuit.Port(frequency=freq, name='port1', z0=50)
        port2 = rf.Circuit.Port(frequency=freq, name='port2', z0=50)
        cnx = [
            [(network,0),(port1,0)],
            [(network,1),(port2,0)]
        ]
        crt = rf.Circuit(cnx)
        np.testing.assert_allclose(crt.s_external, s)

        power = [1,0] # 1 Watt at port 1
        phase = [0,0]
        V_at_ports = crt.voltages_external(power, phase)
        I_at_ports = crt.currents_external(power, phase)
        np.testing.assert_allclose(V_at_ports, [[10+0j, 0+0j]])
        np.testing.assert_allclose(I_at_ports, [[0.2+0j, 0+0j]])

        power = [0,1] # 1 Watt at port 2
        V_at_ports = crt.voltages_external(power, phase)
        I_at_ports = crt.currents_external(power, phase)
        np.testing.assert_allclose(V_at_ports, [[10+0j, 10+0j]])
        np.testing.assert_allclose(I_at_ports, [[-0.2+0j, 0.2+0j]])

if __name__ == '__main__':
    unittest.main()
